package main

import (
	"fmt"
	"strconv"
	"strings"

	"gitee.com/bon-ami/eztools/v4"
)

// Return values: whether added, appended fields, appended values
func prompt1Field(field string, fields, values []string, curr string) ([]string, []string) {
	const forContact = " for this contact:"
	var defVal string
	if len(curr) > 0 {
		defVal = "([Enter]=" + curr + ")"
	}
	val := eztools.PromptStr(field + forContact + defVal)
	if len(val) > 0 {
		return append(fields, field), append(values, val)
	}
	return fields, values
}

func AddContact(db *eztools.Dbs) {
	var (
		ans, team, idStr string
		idInt            int
		err              error
	)
LOOP:
	for {
		idInt, team, err = Add1Contact(db, team)
		if err != nil {
			eztools.LogFatal(err)
		}
	CHOICE:
		ans = strings.ToLower(eztools.PromptStr("Add next, add one for the same Team (default), assign as team Leader, or Exit?"))
		switch ans {
		case "a":
			team = ""
		case "l":
			if idInt != eztools.InvalidID {
				idStr = strconv.Itoa(idInt)
			} else {
				idStr = ""
			}
			err = MakeLeader(db, idStr, team)
			goto CHOICE
		case "e":
			break LOOP
		case "t":
		default:
			continue
		}
	}
}

// Add adds a contact, assigning a team
//  if no team ID provided, all teams are listed,
//  and input is needed to choose one
// Return values: ID of the contact, team ID, error value
func Add1Contact(db *eztools.Dbs, team string) (int, string, error) {
	fields := make([]string, 0)
	values := make([]string, 0)
	for i := 0; i < len(fldsStrs); i++ {
		if i == fldsIndTeam {
			continue
		}
		fields, values = prompt1Field(fldsStrs[i], fields, values, "")
	}
	if len(fields) < 1 {
		return eztools.InvalidID, team, eztools.ErrNoValidResults
	}
	if len(team) < 1 {
		id, err := db.ChoosePairOrAdd(fldsStrs[fldsIndTeam], false, true)
		if err != nil {
			return eztools.InvalidID, team, err
		}
		if id != eztools.InvalidID {
			team = strconv.Itoa(id)
		}
	}
	if len(team) > 0 {
		fields = append(fields, fldsStrs[fldsIndTeam])
		values = append(values, team)
	}
	if eztools.Debugging && eztools.Verbose > 2 {
		eztools.ShowStrln(fields, values)
	}
	id, err := db.AddWtParams(tblContacts, fields, values, true)
	return id, team, err
}

// Modify modifies a contact by ID
func ModifyContact(db *eztools.Dbs, id string) error {
	fields := make([]string, 0)
	values := make([]string, 0)
	curr, err := db.Search(tblContacts, fldId+"="+id, fldsStrs, "")
	if err != nil || len(curr) == 0 {
		eztools.LogPrint("NO current info for this contact", err)
		return err
	}
	if len(curr) != 1 {
		eztools.LogPrint(len(curr), "records found for one contact!")
	}
	var defTm string
	for i := 0; i < len(fldsStrs); i++ {
		if i == fldsIndTeam {
			if len(curr[0][i]) > 0 {
				defTm = "([Enter]=" + curr[0][i] + ")"
			}
			continue
		}
		fields, values = prompt1Field(fldsStrs[i], fields, values, curr[0][i])
	}
	eztools.ShowStrln("Choose a team:" + defTm)
	team, err := db.ChoosePairOrAdd(tblTeam, false, true)
	if err != nil {
		return err
	}
	if team != eztools.InvalidID {
		fields = append(fields, fldsStrs[fldsIndTeam])
		values = append(values, strconv.Itoa(team))
	}
	err = db.UpdateWtParams(tblContacts,
		fldId+"="+id, fields, values, false, false)
	return err
}

// Delete deletes a contact by ID
func DeleteContact(db *eztools.Dbs, id string) error {
	err := db.DeleteWtID(tblContacts, id)
	return err
}

// Parameters: hdr = csv[0]; line = csv[i];
//	existing = append(fldsStrs, fldId)
func updateExistingContact(db *eztools.Dbs, hdr, line, existing []string, indexes []int) error {
	//eztools.LogPrint("line", line)
	newLine := make([]string, len(line))
	copy(newLine, line)
	//eztools.LogPrint("newline", newLine)
	newHdr := make([]string, len(hdr))
	copy(newHdr, hdr)
	if indexes[fldsIndNick] == eztools.InvalidID &&
		indexes[fldsIndName] != eztools.InvalidID {
		// if only name provided, and no nick.
		if len(existing[fldsIndName]) > 0 {
			if existing[fldsIndName] != line[indexes[fldsIndName]] {
				// keep current name and check for current nick
				newNick := newLine[indexes[fldsIndName]]
				newLine[indexes[fldsIndName]] = ""
				if len(existing[fldsIndNick]) > 0 {
					if !strings.Contains(existing[fldsIndNick],
						newNick) {
						newNick += "," + existing[fldsIndNick]
					} else {
						newNick = "" // already there. do nothing
					}
				}
				if len(newNick) > 0 {
					newHdr = append(newHdr, fldsStrs[fldsIndNick])
					newLine = append(newLine, newNick)
				}
			}
		}
	}
	// check for existing field values
	for i := 0; i < len(newLine); i++ {
		//eztools.LogPrint("newline", i, "=", newLine, "=")
		if len(newLine[i]) < 1 {
			continue
		}
		for j := 0; j < len(fldsStrs); j++ {
			if newHdr[i] != fldsStrs[j] {
				continue
			}
			// field name matched
			if len(existing[j]) < 1 {
				break
			}
			if newLine[i] == existing[j] ||
				!eztools.ChkCfmNPrompt("changing "+newHdr[i]+
					" from "+existing[j]+" to "+newLine[i], "n") {
				newLine[i] = ""
			}
			break
		}
	}
	return db.UpdateWtParams(tblContacts,
		fldId+"="+existing[len(fldsStrs)], newHdr, newLine, true, false)
}

func lalo2strings(loc eztools.LocationInfos) []string {
	ret := make([]string, 2)
	ret[0] = fmt.Sprintf("%f", loc.Latitude)
	ret[1] = fmt.Sprintf("%f", loc.Longitude)
	return ret
}

func preMd5FucsGen(map1 eztools.CfgEZtoolsMap) (
	preMd5Params func(eztools.Pairs[string,
		string]) (eztools.Pairs[string, string], string),
	preMd5Whole func(string) string) {
	switch map1.Name {
	case "QQ":
		preMd5Params = eztools.ReorderParams4Map
	case "百度":
		preMd5Whole = eztools.EscapeStr4Map
	case "Bing":
		// only default district can be parsed
		/*for i, v := range map1.Locate.Param {
			if v.Name == "adminDistrict" {
				break
			}
		}*/
	}
	return
}

func address2Position4Contact(db *eztools.Dbs, cfg []eztools.CfgEZtoolsMap,
	row []string, mapLocks []chan struct{},
	mapInd int, mapDone chan struct{}) {
	defer func() {
		mapDone <- struct{}{}
	}()
	lenCfg := len(cfg)
LOOP4MAPS2POSITION:
	for mapID, lenDone := mapInd%lenCfg, 0; lenDone < lenCfg; mapID += 1 {
		lenDone += 1
		if mapID >= lenCfg {
			mapID = 0
		}
		map1 := cfg[mapID]
		if !map1.CanParseAddr() {
			continue
		}
		if eztools.Debugging && eztools.Verbose > 1 {
			eztools.LogPrint("trying to position", row,
				"from", map1.Name)
		}
		preMd5Params, preMd5Whole := preMd5FucsGen(map1)
		<-mapLocks[mapID]
		inf, _, err := map1.Addr2Location(row[0],
			preMd5Params, preMd5Whole)
		mapLocks[mapID] <- struct{}{}
		if eztools.Debugging && eztools.Verbose > 1 {
			eztools.LogPrint(map1.Name, "unlocked")
		}
		if err != nil {
			eztools.LogPrint("FAILED to get position for #"+
				row[1]+": "+row[0], err)
			continue
		}
		switch len(inf) {
		case 0:
			eztools.LogPrint("NO position found for #",
				row[1], row[0])
			continue LOOP4MAPS2POSITION
		case 1:
		default:
			eztools.LogPrint("MULTIPLE positions found for #",
				row[1], row[0])
		}
		for _, inf1 := range inf {
			if inf1.Type >= eztools.LOCATION_TYPE_CBD {
				err = db.UpdateWtParams(tblContacts, fldId+"="+row[1],
					[]string{fldLat, fldLng},
					lalo2strings(inf1), true,
					true /* cannot be false, because of routine */)
				break LOOP4MAPS2POSITION
			} else {
				eztools.LogPrint("not detailed level", inf1.Type,
					inf1.Label, "for #", row[1], row[0])
			}
		}
	}
}

func Address2Position4Contacts(db *eztools.Dbs, cfg []eztools.CfgEZtoolsMap) error {
	selected, err := db.Search(tblContacts, "",
		append([]string{fldAddr}, genFlds...), "")
	if err != nil {
		return err
	}
	mapLocks := make([]chan struct{}, len(cfg))
	for i := range cfg {
		//eztools.ShowStrln("unlocking", i)
		mapLocks[i] = make(chan struct{}, 1)
		mapLocks[i] <- struct{}{}
	}
	mapDone := make(chan struct{})
	mapInd := 0
	for _, con1 := range selected {
		//eztools.ShowStrln(con1[fldsIndLat+1], ",", con1[fldsIndLng+1])
		if len(con1[0]) < 1 {
			continue
		}
		if len(con1[fldsIndLat+1]) > 0 &&
			len(con1[fldsIndLng+1]) > 0 {
			continue
		}
		go address2Position4Contact(db,
			cfg, con1, mapLocks, mapInd, mapDone)
		mapInd += 1
	}
	for i := mapInd; i > 0; i-- {
		//eztools.ShowStrln("waiting", i)
		<-mapDone
	}
	return err
}

func distance2Position4Contact(db *eztools.Dbs, cfg []eztools.CfgEZtoolsMap,
	row []string, location eztools.LocationInfos, distance int, mapLocks []chan struct{},
	mapInd int, mapDone chan string) {
	lenCfg := len(cfg)
LOOP4MAPS2DISTANCE:
	for mapID, lenDone := mapInd%lenCfg, 0; lenDone < lenCfg; mapID += 1 {
		lenDone += 1
		if mapID >= lenCfg {
			mapID = 0
		}
		map1 := cfg[mapID]
		if !map1.CanRouteWalk() {
			continue
		}
		var contactPos eztools.LocationInfos
		contactPos.ParseLaLo(row[1+fldsIndLat], eztools.MAP_REPLY_TYPE_LATI)
		contactPos.ParseLaLo(row[1+fldsIndLng], eztools.MAP_REPLY_TYPE_LONGI)
		if eztools.Debugging && eztools.Verbose > 1 {
			eztools.LogPrint("trying to calculate", row,
				"from", map1.Name)
		}
		preMd5Params, preMd5Whole := preMd5FucsGen(map1)
		<-mapLocks[mapID]
		inf, _, err := map1.CalcRouteWalk([...]eztools.LocationInfos{
			location, contactPos}, preMd5Params, preMd5Whole)
		mapLocks[mapID] <- struct{}{}
		if eztools.Debugging && eztools.Verbose > 1 {
			eztools.LogPrint(map1.Name, "unlocked")
		}
		if err != nil {
			eztools.LogPrint("FAILED to get path from "+
				map1.Name+" for "+row[0], err)
			continue
		}
		switch len(inf) {
		case 0:
			eztools.LogPrint("NO path found from",
				map1.Name, "for", row[0])
			continue LOOP4MAPS2DISTANCE
		}
		for _, inf1 := range inf {
			dist := inf1.Distance
			if inf1.UnitDistance != 0 {
				dist *= inf1.UnitDistance
			}
			if eztools.Debugging && eztools.Verbose > 0 {
				eztools.LogPrint(map1.Name, "said",
					dist, "to", row[0])
			}
			if dist <= float32(distance) {
				mapDone <- row[1]
				return
			}
		}
		//break
	}
	mapDone <- ""
	return
}

func Distance2Position4Contacts(db *eztools.Dbs, cfg []eztools.CfgEZtoolsMap,
	addr string, distance int) error {
	var (
		locations []eztools.LocationInfos
		location  *eztools.LocationInfos
		err       error
	)
LOOP2PARSEADDR:
	for _, map1 := range cfg {
		if !map1.CanRouteWalk() {
			continue
		}
		preMd5Params, preMd5Whole := preMd5FucsGen(map1)
		locations, _, err = map1.Addr2Location(addr, preMd5Params, preMd5Whole)
		if err != nil {
			eztools.LogPrint("FAILED to get position for"+
				addr, err)
			continue
		}
		switch len(locations) {
		case 0:
			eztools.LogPrint("NO position found for", addr)
		case 1:
		default:
			eztools.LogPrint("MULTIPLE positions found", addr)
		}
		for _, inf1 := range locations {
			if inf1.Type >= eztools.LOCATION_TYPE_CBD {
				location = &inf1
				break LOOP2PARSEADDR
			} else {
				eztools.LogPrint("not detailed level", inf1.Type,
					inf1.Label, "for", addr)
			}
		}
	}
	if location == nil {
		return eztools.ErrNoValidResults
	}
	selected, err := db.Search(tblContacts, "",
		append([]string{fldAddr}, genFlds...), "")
	if err != nil {
		return err
	}
	mapLocks := make([]chan struct{}, len(cfg))
	for i := range cfg {
		//eztools.ShowStrln("unlocking", i)
		mapLocks[i] = make(chan struct{}, 1)
		mapLocks[i] <- struct{}{}
	}
	mapDone := make(chan string)
	mapInd := 0
	for _, con1 := range selected {
		//eztools.ShowStrln(con1[fldsIndLat+1], ",", con1[fldsIndLng+1])
		if len(con1[0]) < 1 {
			eztools.LogPrint("NO address configured for #", con1[1])
			continue
		}
		if len(con1[fldsIndLat+1]) < 1 ||
			len(con1[fldsIndLng+1]) < 1 {
			eztools.LogPrint("NO address parsed for #", con1[1])
			continue
		}
		go distance2Position4Contact(db,
			cfg, con1, *location, distance, mapLocks, mapInd, mapDone)
		mapInd += 1
	}
	for i := mapInd; i > 0; i-- {
		//eztools.ShowStrln("waiting", i)
		res := <-mapDone
		if len(res) > 0 {
			name := "anonymous"
			selected, err := db.Search(tblContacts, fldId+"="+res,
				[]string{fldsStrs[fldsIndName]}, "")
			if err == nil && len(selected) > 0 {
				name = selected[0][0]
			}
			eztools.LogPrint("ATTENTION: close to address, #",
				res, name)
		}
	}
	return err
}

/*func checkNAssignMapCenter(db *eztools.Dbs) error {
	cents, err := db.Search(tblMap, "",
		[]string{fldProvince}, " ORDER BY "+fldProvince)
	if err != nil {
		return err
	}
	groups, err := db.Search(tblContacts, fldProvince+" IS NOT NULL AND "+
		fldLat+" IS NOT NULL AND "+fldLng+" IS NOT NULL",
		[]string{fldsStrs[fldsIndName], fldProvince, fldLat, fldLng},
		" ORDER BY "+fldProvince)
	if err != nil {
		return err
	}
	iCents := 0
	for iGroups := 0; iGroups < len(groups); iGroups++ {
		if iCents < len(cents) && groups[iGroups][1] == cents[iCents][0] {
			iCents++
			continue
		}
		choices := make([]string, 0)
		iGroup := iGroups
		for ; iGroup < len(groups) &&
			groups[iGroup][1] == groups[iGroups][1]; iGroup++ {
			choices = append(choices, groups[iGroup][0]+
				"("+groups[iGroup][2]+","+groups[iGroup][3]+")")
		}
		eztools.ShowStrln("Choose a contact to be center of group",
			groups[iGroups][1], "in map")
		idI, _ := eztools.ChooseStrings(choices)
		if idI == eztools.InvalidID {
			return eztools.ErrAbort
		}
		vals := make([]string, 3)
		vals[0] = groups[iGroups][1]
		vals[1] = groups[iGroups+idI][2]
		vals[2] = groups[iGroups+idI][3]
		_, err = db.AddWtParamsUniq(tblMap, []string{fldProvince,
			fldLat, fldLng}, vals, false)
		if err != nil {
			return err
		}
		iGroups = iGroup - 1
	}
	return nil
}*/

/*func AssignSttMapGrp(db *eztools.Dbs) error {
	nulls, err := db.Search(tblContacts, fldProvince+" IS NULL AND "+
		fldLat+" IS NOT NULL AND "+fldLng+" IS NOT NULL",
		[]string{fldId}, "")
	if err != nil {
		return err
	}
	vals, err := db.Search(tblContacts, fldProvince+" IS NOT NULL AND "+
		fldLat+" IS NOT NULL AND "+fldLng+" IS NOT NULL",
		[]string{fldProvince}, " GROUP BY "+fldProvince)
	if err != nil {
		return err
	}
	idStr := ""
	const MAX_IN1GROUP = 1
	cnt := MAX_IN1GROUP
	idGrp := eztools.DefID
	for _, i := range vals {
		id, err := strconv.Atoi(i[0])
		if err != nil {
			eztools.LogPrint(i[0] + " unrecognized as group ID!")
			continue
		}
		if idGrp <= id {
			idGrp = id + 1
		}
	}
	sizeNulls := len(nulls)
	for n, i := range nulls {
		if len(idStr) > 0 {
			idStr += " OR "
		}
		idStr += fldId + "=" + i[0]
		cnt -= 1
		if cnt > 0 && (n+1) != sizeNulls {
			continue
		}
		err = db.UpdateWtParams(tblContacts, idStr, []string{fldProvince},
			[]string{strconv.Itoa(idGrp)}, false, false)
		if err != nil {
			return err
		}
		idStr = ""
		idGrp += 1
		cnt = MAX_IN1GROUP
	}
	err = checkNAssignMapCenter(db)
	return err
}*/

func genSttMap1(db *eztools.Dbs, cfg []eztools.CfgEZtoolsMap,
	row []eztools.LocationInfos, anony, file, ext string,
	mapLocks []chan struct{}, mapInd int,
	mapDone chan struct{}) (err error) {
	defer func() {
		mapDone <- struct{}{}
	}()
	lenCfg := len(cfg)
	for mapID, lenDone := mapInd%lenCfg, 0; lenDone < lenCfg; mapID += 1 {
		lenDone += 1
		if mapID >= lenCfg {
			mapID = 0
		}
		map1 := cfg[mapID]
		for _, mpStatic := range map1.Static {
			if !strings.Contains(mpStatic.Name, anony) {
				continue
			}
			if !mpStatic.CanStaticMap(len(row)) {
				if eztools.Debugging && eztools.Verbose > 1 {
					eztools.LogPrint(map1.Name+"."+mpStatic.Name,
						"cannot hold", len(row))
				}
				continue
			}
			if len(mpStatic.Labels.Center) > 0 {
				continue
			}
			if eztools.Debugging && eztools.Verbose > 1 {
				eztools.LogPrint("trying to map", row,
					"from", map1.Name+"."+mpStatic.Name)
			}
			preMd5Params, preMd5Whole := preMd5FucsGen(map1)
			/*eztools.LogPrint(map1, file+mp.Name+ext, row,
			preMd5Params, preMd5Whole)*/
			<-mapLocks[mapID]
			bodyBytes, err := mpStatic.StaticMap2File(map1, file+map1.Name+ext, row,
				preMd5Params, preMd5Whole)
			mapLocks[mapID] <- struct{}{}
			/*if eztools.Debugging && eztools.Verbose > 1 {
				eztools.LogPrint(map1.Name, "unlocked")
			}*/
			if err != nil {
				if bodyBytes != nil && len(bodyBytes) > 0 {
					eztools.Log(string(bodyBytes))
				}
				eztools.Log(map1.Name+"."+mpStatic.Name, err)
			} else {
				break
			}
		}
	}
	return nil
}

func GenSttMap(db *eztools.Dbs, cfg []eztools.CfgEZtoolsMap,
	anony, fileBase, fileExt string) error {
	/*allRec, err := db.Search(tblContacts, "", []string{
		fldProvince, fldLat, fldLng}, "")
	if err != nil {
		return err
	}
	if len(allRec) < 1 {
		return eztools.ErrNoValidResults
	}
	centArr := make([]eztools.LocationInfos, len(allRec))
	for _, i := range allRec {
		n, err := strconv.Atoi(i[0])
		if err != nil {
			continue
		}
		if n >= len(allRec) {
			continue
		}
		centArr[n].SetLabel(i[0])
		centArr[n].ParseLaLo(i[1], eztools.MAP_REPLY_TYPE_LATI)
		centArr[n].ParseLaLo(i[2], eztools.MAP_REPLY_TYPE_LONGI)
	}*/
	var (
		err   error
		found bool
	)
	cri := [...]string{"NULL", "NOT NULL"}
	recs := make([][][]string, len(cri))
	for i := 0; i < 2; i++ {
		recs[i], err = db.Search(tblContacts, fldProvince+
			" IS "+cri[i]+" AND "+
			fldLat+" IS NOT NULL AND "+fldLng+" IS NOT NULL",
			[]string{fldLat, fldLng, fldsStrs[fldsIndName], fldProvince},
			"")
		if err != nil {
			return err
		}
		if !found && recs[i] != nil && len(recs[i]) > 0 {
			found = true
		}
	}
	if !found {
		return eztools.ErrNoValidResults
	}

	mapLocks := make([]chan struct{}, len(cfg))
	for i := range cfg {
		//eztools.ShowStrln("unlocking", i)
		mapLocks[i] = make(chan struct{}, 1)
		mapLocks[i] <- struct{}{}
	}
	mapDone := make(chan struct{})
	mapInd := 0

	for _, rec := range recs {
		if rec != nil && len(rec) > 0 {
			loc := make([]eztools.LocationInfos, len(rec))
			for i, v := range rec {
				loc[i].ParseLaLo(v[0], eztools.MAP_REPLY_TYPE_LATI)
				loc[i].ParseLaLo(v[1], eztools.MAP_REPLY_TYPE_LONGI)
				loc[i].SetLabel(v[2])
			}
			go genSttMap1(db, cfg, loc, anony,
				fileBase+rec[0][3], fileExt,
				mapLocks, mapInd, mapDone)
			mapInd += 1
		}
	}
	for i := mapInd; i > 0; i-- {
		//eztools.ShowStrln("waiting", i)
		<-mapDone
	}
	return err
}
